package com.qyer.commons.blender;

import static com.qyer.commons.blender.BlenderLayoutPattern.DEFAULT_COUNT_PER_KEY;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.qyer.commons.blender.keyconverter.BlenderKeyConverter;
import com.qyer.commons.blender.keyconverter.StringIntConverter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 * User: Z J Wu Date: 2015/09/24 Time: 00:38 Package: com.qyer.dsp.model B=BLEND-KEY
 * CO=CONTENT-OBJECT
 */
public class Blender<BK, CO> {

  public static final char BLENDER_SEP1 = ',';
  public static final char BLENDER_SEP2 = ':';
  public static final int maxTry = 2000;

  private List<BlenderLayoutPattern<BK>> layoutList;

  private Map<BK, List<CO>> rawMap;
  private List<CO> totalList;
  private Map<BK, Boolean> finishMap;
  private Set<BK> unknownBlendKeys;

  private int getTotalCount(Map<BK, List<CO>> rawMap) {
    int totalCount = 0;
    List<CO> list;
    for (Map.Entry<BK, List<CO>> entry : rawMap.entrySet()) {
      list = entry.getValue();
      if (CollectionUtils.isEmpty(list)) {
        continue;
      }
      totalCount += list.size();
    }
    return totalCount;
  }

  /**
   * 只指定内容, 不指定pattern
   * <p/>
   * 使用全随机混排, 对应每个BlendKey的混排数量见LayoutPattern.DEFAULT_COUNT_PER_KEY
   *
   * @param rawMap
   * @throws Exception
   */
  public Blender(Map<BK, List<CO>> rawMap) throws Exception {
    if (MapUtils.isEmpty(rawMap)) {
      throw new Exception("Raw map of blender is necessary.");
    }
    this.rawMap = rawMap;
    finishMap = new HashMap<>(rawMap.size());
    layoutList = new ArrayList<>(rawMap.size());
    BK key;
    int totalCount = 0;
    for (Map.Entry<BK, List<CO>> entry : rawMap.entrySet()) {
      key = entry.getKey();
      layoutList.add(new BlenderLayoutPattern<>(key));
      totalCount += entry.getValue().size();
      finishMap.put(key, false);
    }
    totalList = new ArrayList<>(totalCount);
  }

  public Blender(String layout, BlenderKeyConverter<BK> converter, Map<BK, List<CO>> rawMap) throws
    Exception {
    this(layout, DEFAULT_COUNT_PER_KEY, converter, rawMap);
  }

  public Blender(List<BlenderLayoutPattern<BK>> layoutList, Map<BK, List<CO>> rawMap) throws
    Exception {
    if (MapUtils.isEmpty(rawMap)) {
      throw new Exception("Raw map of blender is necessary.");
    }
    if (CollectionUtils.isEmpty(layoutList)) {
      throw new Exception("Layout cannot be empty.");
    }
    this.rawMap = rawMap;
    finishMap = new HashMap<>(layoutList.size());
    this.layoutList = layoutList;
    BK blendKey;
    Set<BK> layoutBKs = Sets.newHashSetWithExpectedSize(rawMap.size());
    for (BlenderLayoutPattern<BK> blenderLayoutPattern : layoutList) {
      blendKey = blenderLayoutPattern.getKey();
      layoutBKs.add(blendKey);
      finishMap.put(blendKey, false);
    }
    totalList = new ArrayList<>(getTotalCount(rawMap));
    unknownBlendKeys = Sets.newHashSet(rawMap.keySet());
    unknownBlendKeys.removeAll(layoutBKs);
  }

  /**
   * 根据指定规则混排, 如果不指定每个BLEND_KEY对应的混排数量, 则随机. 随机数目和LayoutPattern一致
   *
   * @param layout
   * @param converter
   * @param rawMap
   * @throws Exception
   */
  public Blender(String layout, int contentPerBlendKey, BlenderKeyConverter<BK> converter,
                 Map<BK, List<CO>> rawMap) throws Exception {
    if (MapUtils.isEmpty(rawMap)) {
      throw new Exception("Raw map of blender is necessary.");
    }
    if (StringUtils.isBlank(layout)) {
      throw new Exception("Layout string cannot be empty.");
    }
    String[] layoutArray = StringUtils.split(layout, BLENDER_SEP1);
    if (ArrayUtils.isEmpty(layoutArray)) {
      throw new Exception("Layout string must contain at least 1 pattern(" + layout + ")");
    }
    this.rawMap = rawMap;
    int i, num;
    String keyStr;
    finishMap = new HashMap<>(layoutArray.length);
    layoutList = new ArrayList<>(layoutArray.length);
    BK blendKey;
    boolean randomBlendKeyCount;
    Random random = new Random();
    Set<BK> layoutBKs = Sets.newHashSetWithExpectedSize(rawMap.size());
    for (String pattern : layoutArray) {
      i = pattern.indexOf(BLENDER_SEP2);
      randomBlendKeyCount = i < 0;
      if (randomBlendKeyCount) {
        keyStr = pattern;
        num = nextInt(random, contentPerBlendKey);
      } else {
        keyStr = pattern.substring(0, i);
        num = Integer.parseInt(pattern.substring(i + 1));
      }
      blendKey = converter.convert(keyStr);
      layoutBKs.add(blendKey);
      layoutList.add(new BlenderLayoutPattern<>(blendKey, num));
      finishMap.put(blendKey, false);
    }
    totalList = new ArrayList<>(getTotalCount(rawMap));
    unknownBlendKeys = Sets.newHashSet(rawMap.keySet());
    unknownBlendKeys.removeAll(layoutBKs);
  }

  public List<CO> getTotalList() {
    return totalList;
  }

  public void blend() throws Exception {
    blend(0);
  }

  public void blend(int maxRepeatCount) throws Exception {
    if (MapUtils.isEmpty(rawMap)) {
      return;
    }
    int cnt = 0, subCount, from, to, partSize;
    List<CO> part;
    int finishedCnt = 0;
    Map<BK, Integer> posMap = new HashMap<>(rawMap.size());
    for (Map.Entry<BK, ?> entry : rawMap.entrySet()) {
      posMap.put(entry.getKey(), 0);
    }
    int pos;
    for (; ; ) {
      // 这个值理论上不会到达. 这里是为了避免死循环的防御
      if (cnt >= maxTry) {
        throw new Exception("Too many input after " + maxTry + " rounds.");
      }
      if (finishedCnt >= layoutList.size()) {
        break;
      }
      BK blendKey;
      for (BlenderLayoutPattern<BK> pattern : layoutList) {
        blendKey = pattern.getKey();
        if (finishMap.get(blendKey)) {
          continue;
        }
        subCount = pattern.getRepeatNum(maxRepeatCount);
        part = rawMap.get(blendKey);
        if (CollectionUtils.isEmpty(part)) {
          ++finishedCnt;
          finishMap.put(blendKey, true);
          continue;
        }
        partSize = part.size();
        pos = posMap.get(blendKey);
        from = pos;
        if (from >= partSize) {
          ++finishedCnt;
          finishMap.put(blendKey, true);
          continue;
        }
        to = from + subCount;
        if (to > partSize) {
          to = partSize;
        }
        totalList.addAll(part.subList(from, to));
        posMap.put(blendKey, to);
      }
      ++cnt;
    }
    // 不认识的BlendKey也要添加到最终结果
    if (CollectionUtils.isNotEmpty(unknownBlendKeys)) {
      for (BK unknownBlendKey : unknownBlendKeys) {
        finishMap.put(unknownBlendKey, true);
        totalList.addAll(rawMap.get(unknownBlendKey));
      }
    }
  }

  public List<BlenderLayoutPattern<BK>> getLayoutList() {
    return layoutList;
  }

  public static int nextInt(Random random, int max) {
    return random.nextInt(max) + 1;
  }

  public static void main(String[] args) throws Exception {
    Map<Integer, List<String>> dataMap = new HashMap<>();
    List<String> s;
    int totalSize = 0;
    Random random = new Random();
    for (int i = 1; i <= 4; i++) {
      int cnt = random.nextInt(10) + 5;
      s = new ArrayList<>(cnt);
      for (int j = 0; j < cnt; j++) {
        ++totalSize;
        s.add(StringUtils.repeat(String.valueOf(i), i));
      }
      dataMap.put(i, s);
    }
    System.out.println(dataMap);
    System.out.println(totalSize);

    System.out.println("---------------------------------");
    String layout = "1:5,2:2,4";
    Blender<Integer, String> blender = new Blender<>(layout, 5, new StringIntConverter(), dataMap);
    blender.blend();
    System.out.println(blender.getLayoutList());
    System.out.println(blender.getTotalList());
    System.out.println(blender.getTotalList().size());
    System.out.println("---------------------------------");
    List<BlenderLayoutPattern<Integer>> blenderLayoutPatterns = Lists
      .newArrayListWithExpectedSize(3);
    blenderLayoutPatterns.add(new BlenderLayoutPattern<>(1, 3));
    blenderLayoutPatterns.add(new BlenderLayoutPattern<>(2));
    blenderLayoutPatterns.add(new BlenderLayoutPattern<>(4));
    blender = new Blender<>(blenderLayoutPatterns, dataMap);
    blender.blend();
    System.out.println(blender.getLayoutList());
    System.out.println(blender.getTotalList());
    System.out.println(blender.getTotalList().size());
    System.out.println("---------------------------------");
    blender = new Blender<>(dataMap);
    blender.blend();
    System.out.println(blender.getLayoutList());
    System.out.println(blender.getTotalList());
    System.out.println(blender.getTotalList().size());
  }
}
